#include <gst/app/gstappsrc.h>
#include <gst/app/gstappsink.h>
#include <string.h>
#include <glib/gprintf.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static bool s_isErrOccur = false;
static char s_errorMsg[512] = {0};
static pthread_t s_gstThread = -1;
static bool s_isGstThreadStarted = false;
static pthread_mutex_t s_writeLock = PTHREAD_MUTEX_INITIALIZER;
static GstState s_appSrcState = GST_STATE_PAUSED;
static pthread_cond_t s_condForOpen = PTHREAD_COND_INITIALIZER;

typedef struct NMDioVideoHandler {
    GstElement *pipeline;
    GstElement *source;
} NMDioVideoHandler;

NMDioVideoHandler* s_handle = NULL;

static void appNeedData(GstAppSrc *src, guint length, gpointer user_data)
{
    g_printf("####################### appNeedData enter!\n");
    if (GST_STATE_PAUSED != s_appSrcState) {
        NMDioVideoHandler* hdl = (NMDioVideoHandler*)user_data;
        if (NULL != hdl) {
            s_appSrcState = GST_STATE_PAUSED;
            gst_element_set_state (hdl->pipeline, s_appSrcState);
        }
    }
}

static void appEnoughData(GstAppSrc *src, gpointer user_data)
{
    g_printf("####################### appEnoughData enter!\n");
}

static gboolean appSeekData(GstAppSrc *src, guint64 offset, gpointer user_data)
{
    g_printf("####################### appSeekData enter!\n");
    return FALSE;
}

static GstAppSrcCallbacks s_callBacks = {
    appNeedData,
    appEnoughData,
    appSeekData
};

static gboolean bus_call (GstBus *bus,GstMessage *msg, gpointer data){
    GMainLoop *loop = (GMainLoop *) data;

    switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_EOS:
        g_print ("####################### Stream Ends\n");
        g_main_loop_quit (loop);
        break;

    case GST_MESSAGE_ERROR: {
        gchar  *debug;
        GError *error;

        gst_message_parse_error (msg, &error, &debug);
        g_free (debug);

        strncpy(s_errorMsg, error->message, 512);
        s_isErrOccur = true;

        g_printerr ("####################### Error: %s\n", error->message);
        g_error_free (error);

        g_main_loop_quit (loop);
        exit(0);
        break;
    }
    case GST_MESSAGE_STATE_CHANGED: {
        GstState oldState;
        GstState newState;
        GstState pendingState;
        gst_message_parse_state_changed(msg, &oldState, &newState, &pendingState);
        g_printf("####################### oldState:%d, newState:%d, pendingState:%d!\n", oldState, newState, pendingState);
        break;
    }
    case GST_MESSAGE_STREAM_STATUS: {
        GstStreamStatusType statusType;
        GstElement* owner = NULL;

        gst_message_parse_stream_status(msg, &statusType, &owner);
        g_printf("####################### statusType:%d, owner:%p!\n", statusType, owner);
        break;
    }
    default:
        break;
    }

    return TRUE;
}

static void initArguments() {
    s_isErrOccur = false;
    memset(s_errorMsg, 0, sizeof(s_errorMsg));
    s_gstThread = -1;
    s_isGstThreadStarted = false;
    s_appSrcState = GST_STATE_PAUSED;
}

static void* gstThreadMain(void * args) {
    GMainLoop *loop;
    GstBus *bus;
    GstAppSrc *appsrc;
    GstCaps* appsrccaps;

    if (s_handle) {
        g_printf("thread is created, cannot create it again!");
        return NULL;
    }

    initArguments();

    g_printf("####################### gstThreadMain starting!\n");

    // int size = *((int*)(args));
    int width = 800;// (size >> 16) & 0xFFFF;
    int height = 480;//ssize & 0xFFFF;

    g_printf("####################### gstThreadMain gst_init!\n");
    gst_init (0, 0);

    g_printf("####################### gstThreadMain g_main_loop_new!\n");
    loop = g_main_loop_new (NULL, FALSE);

    g_printf("####################### gstThreadMain memset!\n");
    s_handle = (NMDioVideoHandler*)malloc(sizeof(NMDioVideoHandler));
    memset(s_handle, 0, sizeof(NMDioVideoHandler));
    appsrccaps = gst_caps_new_simple("video/x-raw-rgb",
                                     "width", G_TYPE_INT, width,
                                     "height", G_TYPE_INT, height,
                                     NULL);

    /* Create Pipe's Elements */
    g_printf("####################### gstThreadMain pipeline new!\n");
    s_handle->pipeline = gst_pipeline_new ("NMDioVidioPlayer");
    g_assert (s_handle->pipeline);

    g_printf("####################### gstThreadMain make appsrc!\n");
    s_handle->source   = gst_element_factory_make ("appsrc", GST_ELEMENT_FACTORY_KLASS_SRC);
    g_assert (s_handle->source);

    g_printf("####################### gstThreadMain make h264parse!\n");
    GstElement* parser = gst_element_factory_make("h264parse", GST_ELEMENT_FACTORY_KLASS_PARSER);
    g_assert (parser);
#ifndef PC_TEST
    g_printf("####################### gstThreadMain make vpudec!\n");
    GstElement* decoder = gst_element_factory_make("vpudec", GST_ELEMENT_FACTORY_KLASS_DECODER);
    g_assert (decoder);

    g_printf("####################### gstThreadMain make mfw_ipucsc!\n");
    GstElement* ipucsc = gst_element_factory_make("mfw_ipucsc", "ipucsc");
    g_assert (ipucsc);

    g_printf("####################### gstThreadMain make waylandsink!\n");
    GstElement* videosink = gst_element_factory_make ("waylandsink", GST_ELEMENT_FACTORY_KLASS_SINK) ;
    g_assert (videosink);
#else
    g_printf("####################### gstThreadMain make vpudec!\n");
    GstElement* decoder = gst_element_factory_make("ffdec_h264", GST_ELEMENT_FACTORY_KLASS_DECODER);
    g_assert (decoder);

    g_printf("####################### gstThreadMain make xvimagesink!\n");
    GstElement* videosink = gst_element_factory_make ("xvimagesink", GST_ELEMENT_FACTORY_KLASS_SINK) ;
    g_assert (videosink);
#endif

    /*Make sure: Every elements was created ok*/
    if (!s_handle->pipeline
            || !s_handle->source
            || !parser
            || !decoder
        #ifndef PC_TEST
            || !ipucsc
        #endif
            || !videosink) {
        g_printerr ("One of the elements wasn't create... Exiting\n");
        return NULL;
    }

    appsrc = GST_APP_SRC(s_handle->source);
    gst_app_src_set_callbacks(appsrc, &s_callBacks, s_handle, 0);

    g_object_set (G_OBJECT (videosink), "sync", FALSE, NULL);
    g_object_set (G_OBJECT (videosink), "synchronous", TRUE, NULL);

#ifndef PC_TEST
    g_object_set (G_OBJECT (decoder), "framerate-nu", 1, NULL);
    g_object_set (G_OBJECT (decoder), "framerate-de", 10, NULL);
    g_object_set (G_OBJECT (decoder), "output-format", 1, NULL);
#endif

    g_printf("####################### gstThreadMain watch bus!\n");

    bus = gst_pipeline_get_bus (GST_PIPELINE (s_handle->pipeline));
    gst_bus_add_watch (bus, bus_call, loop);
    gst_object_unref (bus);

    g_printf("####################### gstThreadMain add link!\n");

    /* Add Elements to the Bin */
    gst_bin_add_many(GST_BIN (s_handle->pipeline),
                     s_handle->source,
                     parser,
                     decoder,
                 #ifndef PC_TEST
                     ipucsc,
                 #endif
                     videosink,
                     NULL);

    if (!gst_element_link_many (s_handle->source,
                                parser,
                                decoder,
                            #ifndef PC_TEST
                                ipucsc,
                            #endif
                                videosink,
                                NULL)){
        g_warning ("Linking part (B) Fail...");
    }

    // g_printf("####################### gstThreadMain change state!\n");

    // s_appSrcState = GST_STATE_READY;
    // gst_element_set_state (handle->pipeline, s_appSrcState);
    // s_appSrcState = GST_STATE_PAUSED;
    // gst_element_set_state (handle->pipeline, s_appSrcState);

    s_isGstThreadStarted = 1;

    g_printf("####################### gstThreadMain entering loop!\n");

    pthread_mutex_lock(&s_writeLock);
    pthread_cond_signal(&s_condForOpen);
    pthread_mutex_unlock(&s_writeLock);

    g_main_loop_run (loop); // TODO : will change after!!!

    g_print ("End of the Streaming... ending the playback\n");
    gst_element_set_state (s_handle->pipeline, GST_STATE_NULL);

    /* Eliminating Pipeline */
    g_print ("Eliminating Pipeline\n");
    gst_object_unref (GST_OBJECT (s_handle->pipeline));

    return (void*)0;
}

void* NMDioVideoOpen(int width, int height) {
    int size = (width << 16 | height);
    if (!s_isGstThreadStarted) {
        pthread_mutex_init(&s_writeLock, NULL);
        pthread_cond_init(&s_condForOpen, NULL);

        int ret = pthread_create(&s_gstThread, NULL, &gstThreadMain, (void*)&size);
        pthread_mutex_lock(&s_writeLock);

        if (0 != ret) {
            fprintf(stderr, "create gstThread failed!\n");
            pthread_mutex_unlock(&s_writeLock);
            return NULL;
        }
        pthread_cond_wait(&s_condForOpen, &s_writeLock);
        pthread_mutex_unlock(&s_writeLock);
    }

    return s_handle;
}

void NMDioVideoDisplay(void *argHandle, const char *packetData, int
                       packetSize)
{
    if (s_isErrOccur) {
        g_printf("####################### %s\n",s_errorMsg);
        return;
    }
    if (!argHandle) {
        g_printf("argHandle is NULL!\n");
        return;
    }

    g_printf("####################### NMDioVideoDisplay begin!\n");
    NMDioVideoHandler* hdl = (NMDioVideoHandler*)argHandle;
    if (!hdl) {
        GST_ERROR_OBJECT(hdl, "hdl is invalid, donothing!\n");
        return;
    }

    GstAppSrc* src = GST_APP_SRC(hdl->source);

    g_printf("####################### NMDioVideoDisplay creating Buffer!\n");
    GstBuffer* buffer = gst_buffer_new_and_alloc(packetSize);

    g_printf("####################### NMDioVideoDisplay copy data to Buffer!\n");
    memcpy(GST_BUFFER_DATA(buffer), packetData, packetSize);
    GST_BUFFER_OFFSET(buffer) = 0;
    GST_BUFFER_OFFSET_END(buffer) = packetSize;

    GstFlowReturn ret = gst_app_src_push_buffer(src, buffer);
    g_printf("####################### NMDioVideoDisplay push buffer, result(%d)!\n", ret);

    if (ret != GST_FLOW_OK && ret != GST_FLOW_UNEXPECTED) {
        GST_ELEMENT_ERROR(src, CORE, FAILED, (0), (0));
    }

    if (GST_STATE_PLAYING != s_appSrcState) {
        s_appSrcState = GST_STATE_PLAYING;
        gst_element_set_state (hdl->pipeline, s_appSrcState);
    }
    g_printf("####################### NMDioVideoDisplay end!\n");
}

void NMDioVideoClose(void *argHandle)
{
    pthread_exit(&s_gstThread);
}

int main(int argc, char* argv[]) {
    char filename[128];
    memset(filename, 0, sizeof(filename));
    if (2 > argc) {
        printf("please input filename:");
        fgets(filename, 128, stdin);
    }
    else {
        strncpy(filename, argv[1], 128);
    }

    int fileinfd = open(filename, O_RDONLY);
    if (0 > fileinfd) {
        printf("file(%s) is not exist or cannot open!\n", filename);
        return 1;
    }

    char buf[1024];
    int readsize = 0;
    void* hdl = NMDioVideoOpen(800, 480);
    do {
        readsize = read(fileinfd, buf, 1024);
        if (0 < readsize) {
            NMDioVideoDisplay(hdl, buf, readsize);
//            sleep(1);
        }
    } while(readsize > 0);

    NMDioVideoClose(hdl);
    return 0;
}
